//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//-------------------------------------------------------------------------------------------------

using UnityEngine;
using Flare.Geom;
using System;

namespace Flare.Display
{
    /// <summary>
    /// The GraphicsGradientFill class defines a gradient fill. The fill can be a
    /// linear gradient or radial gradient.
    /// </summary>
    public class GraphicsGradientFill
    {
        /// <summary>
        /// Array of alpha values corresponding to the associated colors.
        /// </summary>
        public float[] alphas { get; set; }

        /// <summary>
        /// Array of RGB colors encoded using hexadecimal values (e.g., 0xFF0000 is red,
        /// 0x00FF00 is green, and 0x0000FF is blue). Up to 15 colors can be specified
        /// for the gradient. Each color must have a corresponding value in the alphas
        /// and ratios properties.
        /// </summary>
        public uint[] colors { get; set; }

        /// <summary>
        /// Array of sequentially increasing ratios corresponding to the associated
        /// colors. Each ratio specifies where the associated color is sampled at 100%
        /// in the gradient box (0 is on the left and 255 is on the right).
        /// </summary>
        public byte[] ratios { get; set; }

        /// <summary>
        /// Transformation matrix for the gradient fill.
        /// </summary>
        public Matrix matrix { get; set; }

        /// <summary>
        /// Location of the focal point for a radial gradient. A value of 0 sets the
        /// focal point at the center of the gradient, while a value of +1 or -1 sets
        /// the focal point at the borders of the gradient.
        /// </summary>
        public float focalPointRatio { get; set; }

        /// <summary>
        /// Value from the InterpolationMethod class indicating the type of color
        /// interpolation to use (e.g., RGB or LINEAR_RGB).
        /// </summary>
        public string interpolationMethod { get; set; }

        /// <summary>
        /// Value from the SpreadMethod class indicating which spread method to use
        /// (e.g., PAD, REFLECT, REPEAT).
        /// </summary>
        public string spreadMethod { get; set; }

        /// <summary>
        /// Value from the GradientType class indicating which type of gradient to use
        /// (e.g., LINEAR, RADIAL).
        /// </summary>
        public string type { get; set; }

        private Texture2D cachedTexture;

        /// <summary>
        /// Create a gradient fill using the specified values.
        /// </summary>
        public GraphicsGradientFill(string type = GradientType.LINEAR,
            uint[] colors = null, float[] alphas = null, byte[] ratios = null,
            Matrix matrix = null, string spreadMethod = SpreadMethod.PAD,
            string interpolationMethod = InterpolationMethod.RGB,
            float focalPointRatio = 0.0f)
        {
            this.type = type;
            this.colors = colors;
            this.alphas = alphas;
            this.ratios = ratios;
            this.matrix = matrix;
            this.spreadMethod = spreadMethod;
            this.interpolationMethod = interpolationMethod;
            this.focalPointRatio = focalPointRatio;
            this.cachedTexture = null;
        }

        /// <summary>
        /// If any of the properties (e.g., alphas, colors, etc.) are modified then MarkDirty
        /// must be explictly called to ensure the modified values are used during rendering.
        /// </summary>
        public void MarkDirty()
        {
            this.cachedTexture = null;
        }

        internal Texture2D texture
        {
            get {
                if (cachedTexture == null)
                {
                    ValidateParameters();

                    cachedTexture = new Texture2D(256, 1, TextureFormat.ARGB32, true);

                    for (int x = 0; x <= this.ratios[0]; ++x)
                    {
                        cachedTexture.SetPixel(x, 0, PremultiplyAlpha(GetColor(0)));
                    }

                    int index = 1;
                    
                    if (this.interpolationMethod.Equals("linearRGB"))
                    {
                        for (int x = this.ratios[0] + 1; x <= this.ratios[this.ratios.Length - 1]; ++x)
                        {
                            Color prev = ConvertToLinearRGB(GetColor(index - 1));
                            Color curr = ConvertToLinearRGB(GetColor(index));
                            Color c = ConvertToSRGB(Color.Lerp(prev, curr,
                                (float)(x - this.ratios[index - 1]) /
                                (float)(this.ratios[index] - this.ratios[index - 1])));
                            
                            cachedTexture.SetPixel(x, 0, PremultiplyAlpha(c));
                            if (x == this.ratios[index])
                            {
                                index++;
                            }
                        }
                    }
                    else
                    {
                        for (int x = this.ratios[0] + 1; x <= this.ratios[this.ratios.Length - 1]; ++x)
                        {
                            Color prev = GetColor(index - 1);
                            Color curr = GetColor(index);
                            Color c = Color.Lerp(prev, curr,
                                (float)(x - this.ratios[index - 1]) /
                                (float)(this.ratios[index] - this.ratios[index - 1]));
                            
                            cachedTexture.SetPixel(x, 0, PremultiplyAlpha(c));
                            if (x == this.ratios[index])
                            {
                                index++;
                            }
                        }
                    }
        
                    for (int x = this.ratios[this.ratios.Length - 1] + 1; x < 256; ++x)
                    {
                        cachedTexture.SetPixel(x, 0, PremultiplyAlpha(GetColor(this.ratios.Length - 1)));
                    }

                    cachedTexture.Apply();
                    cachedTexture.wrapMode = TextureWrapMode.Clamp;
                    cachedTexture.filterMode = FilterMode.Bilinear;
                }

                return cachedTexture;
            }
        }

        private Color GetColor(int i)
        {
            uint color = this.colors[i];
            float alpha = Mathf.Clamp01(this.alphas[i]);

            return new Color(((color >> 16) & 0xff) / 255.0f, ((color >> 8) & 0xff) / 255.0f,
                (color & 0xff) / 255.0f, alpha);
        }

        private void ValidateParameters()
        {
            if ((this.ratios == null) || (this.colors == null) || (this.alphas == null) ||
                (this.ratios.Length < 1) || (this.colors.Length < 1) || (this.alphas.Length < 1) ||
                (this.ratios.Length != this.alphas.Length) ||
                (this.ratios.Length != this.colors.Length))
            {
                throw new global::System.InvalidOperationException(
                    "Gradient parameters are invalid!");
            }

            for (int i = 1; i < this.ratios.Length; ++i)
            {
                if (this.ratios[i - 1] > this.ratios[i])
                {
                    throw new global::System.InvalidOperationException(
                        "Gradient ratios are invalid!");
                }
            }
        }
        
        private Color ConvertToLinearRGB(Color input)
        {
            Color output;
            output.r = ConvertToLinearComponent(input.r);
            output.g = ConvertToLinearComponent(input.g);
            output.b = ConvertToLinearComponent(input.b);
            output.a = input.a;
            return output;
        }
        
        private Color ConvertToSRGB(Color input)
        {
            Color output;
            output.r = ConvertToSRGBComponent(input.r);
            output.g = ConvertToSRGBComponent(input.g);
            output.b = ConvertToSRGBComponent(input.b);
            output.a = input.a;
            return output;
        }
        
        private float ConvertToLinearComponent(float input)
        {
            if (input <= 0.04045f)
            {
                return input * 12.92f;
            }
            else
            {
                return (float)Math.Pow((input + 0.055) / 1.055, 2.4);
            }                
        }
        
        private float ConvertToSRGBComponent(float input)
        {
            if (input <= 0.0031308)
            {
                return input * 12.92f;
            }
            else
            {
                return (1.055f * ((float) Math.Pow(input, (1.0 / 2.4))) - 0.055f);
            }
        }

        private static Color PremultiplyAlpha(Color c)
        {
            return new Color(c.r * c.a, c.g * c.a, c.b * c.a, c.a);
        }
    }
}